
目标描述是后端实现的核心，使用TableGen语言编写，并定义了架构的基本属性，例如：寄存器和指令格式以及用于指令选择的模式。若不熟悉TableGen语言，建议先阅读本书第8章。基本定义在llvm/include/llvm/Target/Target文件中，该文件可在\url{https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/Target/Target.td}上找到。该文件有大量注释，都是关于定义使用的信息来源。

我们将根据目标描述生成整个后端。这个目标还没有实现，稍后需要扩展生成的代码。由于其大小，目标描述分割成几个文件。顶级文件是M88k.td，llvm/lib/Target/M88k目录下，该目录还包括其他文件。我们从寄存器定义开始。

\mySubsubsection{11.4.1.}{添加寄存器定义}

CPU架构通常定义一组寄存器，这些寄存器的特性可以有所不同，一些架构允许访问子寄存器。例如，x86架构有特殊的寄存器名来访问寄存器值的一部分，其他架构没有实现这个。除了通用寄存器、浮点寄存器和向量寄存器外，架构还可以有用于状态码或浮点操作配置的特殊寄存器。我们需要为LLVM定义所有这些信息，寄存器定义存储在M88kRegisterInfo中，在llvm/lib/Target/M88k目录中也可以找到。

M88k架构定义了通用寄存器、用于浮点操作的扩展寄存器和控制寄存器。为了保持示例较小，只定义通用寄存器。首先为寄存器定义一个超类，寄存器有名称和编码。该名称用于指令的文本表示。类似地，编码用作指令二进制表示的一部分。该架构定义了32个寄存器，寄存器的编码使用5位，限制了保存编码的字段。还定义了所有生成的C++代码，应该放在在M88k命名空间中:

\begin{cpp}
class M88kReg<bits<5> Enc, string n> : Register<n> {
    let HWEncoding{15-5} = 0;
    let HWEncoding{4-0} = Enc;
    let Namespace = "M88k";
}
\end{cpp}

接下来，可以定义所有32个通用寄存器。r0寄存器很特殊，在读取时总是返回常量0，所以将该寄存器的isConstant标志设置为true:

\begin{cpp}
foreach I = 0-31 in {
    let isConstant = !eq(I, 0) in
        def R#I : M88kReg<I, "r"#I>;
}
\end{cpp}

对于寄存器分配器，需要将单个寄存器分组到寄存器类中。寄存器的顺序，定义了分配顺序。寄存器分配器还需要有关寄存器的其他信息，可以存储在寄存器中的值类型、寄存器的溢出大小(以位为单位)，以及在内存中所需的对齐方式。没有直接使用RegisterClass基类，而是创建了一个新的M88kRegisterClass类。根据需要更改参数列表，还避免了用于生成代码的C++命名空间名称的重复，这是RegisterClass类的第一个参数:

\begin{cpp}
class M88kRegisterClass<list<ValueType> types, int size,
                        int alignment, dag regList,
                        int copycost = 1>
    : RegisterClass<"M88k", types, alignment, regList> {
    let Size = size;
    let CopyCost = copycost;
}
\end{cpp}

此外，还定义了一个寄存器操作数类。操作数描述指令的输入和输出，指令的组装和分解过程中使用，也在指令选择阶段使用的模式中使用。使用自己的类，我们可以给用于解码寄存器操作数函数，一个符合LLVM编码准则的名称:

\begin{cpp}
class M88kRegisterOperand<RegisterClass RC>
: RegisterOperand<RC> {
    let DecoderMethod = "decode"#RC#"RegisterClass";
}
\end{cpp}

基于这些定义，现在定义通用寄存器。注意，m88k架构的通用寄存器是32位宽的，可以保存整数和浮点值。为了避免写入所有寄存器名，使用序列生成器，基于模板字符串生成字符串列表:

\begin{cpp}
def GPR : M88kRegisterClass<[i32, f32], 32, 32,
                            (add (sequence "R%u", 0, 31))>;
\end{cpp}

同样，我们定义了寄存器操作数。r0寄存器很特殊，它只包含常数0。全局指令选择框架可以使用这一事实，因此将该信息添加到寄存器操作数上:

\begin{cpp}
def GPROpnd : M88kRegisterOperand<GPR> {
    let GIZeroRegister = R0;
}
\end{cpp}

m88k架构有一个扩展，为浮点值定义了一个扩展的寄存器文件，可与通用寄存器相同的方式定义这些寄存器。

通用寄存器也成对使用，主要用于64位浮点运算，需要对它们进行建模。使用sub\_hi和sub\_lo子寄存器索引来描述高32位和低32位，还需要为生成的代码设置C++命名空间:

\begin{cpp}
let Namespace = "M88k" in {
    def sub_hi : SubRegIndex<32, 0>;
    def sub_lo : SubRegIndex<32, 32>;
}
\end{cpp}

使用RegisterTuples类定义寄存器对，该类将子寄存器索引列表作为第一个参数，并将寄存器列表作为第二个参数。只需要偶数/奇数对，就可通过sequence的第四个可选参数来实现这一点，这是在生成序列时使用的步幅:

\begin{cpp}
def GRPair : RegisterTuples<[sub_hi, sub_lo],
                            [(add (sequence "R%u", 0, 30, 2)),
                            (add (sequence "R%u", 1, 31, 2))]>;
\end{cpp}

要使用寄存器对，需要定义一个寄存器类和一个寄存器操作数:

\begin{cpp}
def GPR64 : M88kRegisterClass<[i64, f64], 64, 32,
                              (add GRPair), /*copycost=*/ 2>;
def GPR64Opnd : M88kRegisterOperand<GPR64>;
\end{cpp}

注意，我们将copycost参数设置为2，因为需要两个指令而不是一个指令来将一个寄存器对复制到另一个寄存器对。

这就完成了对寄存器的定义。下一节中，我们将定义指令格式。

\mySubsubsection{11.4.2.}{定义指令格式和指令信息}

指令是使用TableGen指令类定义的。定义一条指令是一项复杂的任务，必须考虑许多细节，指令具有供汇编程序和反汇编程序使用的文本表示形式。有一个名称，并且可以有操作数。汇编程序将文本表示转换为二进制格式，必须定义该格式的布局。对于指令选择，需要给指令附加一个模式。为了管理这种复杂性，需要定义一个类层次结构。基类将描述各种指令格式，并存储在M88kIntrFormats.td文件中，指令本身和指令选择所需的其他定义存储在M88kInstrInfo.td文件中。

首先为m88k架构指令定义一个名为M88kInst的类，可从预定义的指令类派生出这个类。新类有几个参数，outs和ins参数使用特殊的dag类型将输出和输入操作数描述为一个列表。指令的文本表示分为asm参数中给出的助记符和操作数，pattern参数可以保存用于指令选择的模式。

还要定义两个新字段:

\begin{itemize}
\item
Inst字段用于保存指令的位模式。因为指令的大小取决于平台，所以这个字段不能预先定义。m88k架构的所有指令都是32位宽的，所以该字段为bits<32>类型。

\item
另一个字段称为SoftFail，与Inst具有相同的类型。保存一个位掩码，用于指令的实际编码可以与Inst字段中的位不同，但仍然有效。唯一需要这个的平台是ARM，所以可以简单地将这个字段设置为0。
\end{itemize}

其他字段在超类中定义，只设置值。TableGen语言中可以进行简单的计算，为AsmString字段创建值时使用，AsmString字段包含完整的汇编程序表示。若操作数的操作数字符串为空，AsmString字段将只包含asm参数的值；否则，将是两个字符串的连接，它们之间有一个空格:

\begin{cpp}
class InstM88k<dag outs, dag ins, string asm, string operands,
                list<dag> pattern = []>
    : Instruction {
    bits<32> Inst;
    bits<32> SoftFail = 0;
    let Namespace = "M88k";
    let Size = 4;
    dag OutOperandList = outs;
    dag InOperandList = ins;
    let AsmString = !if(!eq(operands, ""), asm,
                        !strconcat(asm, " ", operands));
    let Pattern = pattern;
    let DecoderNamespace = "M88k";
}
\end{cpp}

对于指令编码，制造商通常将指令分组在一起，一组指令具有相似的编码。可以使用这些组系统地创建定义指令格式的类。例如，m88k架构的所有逻辑操作都将目标寄存器编码为21到25位，将第一个源寄存器编码为16到20位。请注意这里的实现模式:为值声明了rd和rs1字段，并将这些值赋给了之前在超类中定义的Inst字段的正确位:

\begin{cpp}
class F_L<dag outs, dag ins, string asm, string operands,
          list<dag> pattern = []>
        : InstM88k<outs, ins, asm, operands, pattern> {
    bits<5> rd;
    bits<5> rs1;
    let Inst{25-21} = rd;
    let Inst{20-16} = rs1;
}
\end{cpp}

有几组基于这种格式的逻辑操作。其中一种是使用三个寄存器的指令组，在手册中称为三元寻址模式:

\begin{cpp}
class F_LR<bits<5> func, bits<1> comp, string asm,
            list<dag> pattern = []>
    : F_L<(outs GPROpnd:$rd), (ins GPROpnd:$rs1, GPROpnd:$rs2),
            !if(comp, !strconcat(asm, ".c"), asm),
            "$rd, $rs1, $rs2", pattern> {
    bits<5> rs2;
    let Inst{31-26} = 0b111101;
    let Inst{15-11} = func;
    let Inst{10} = comp;
    let Inst{9-5} = 0b00000;
    let Inst{4-0} = rs2;
}
\end{cpp}

让我们更详细地研究一下这个类提供的功能。函数形参指定操作，作为一个特殊的特性，第二个操作数可以在操作之前进行补充，可以通过将标志comp设置为1来表示。助记符在asm参数中给出，并可以传递指令选择模式。

通过初始化超类，可以提供更多信息。and指令的完整汇编文本模板是and \$rd，\$rs1，\$rs2。对于该组的所有指令，操作数字符串是固定的，所以可以在这里定义。助记符由该类的用户提供，可以在这里连接.c后缀，这表示应该首先补充第二个操作数。最后，可以定义输出和输入操作数，这些操作数表示为有向无环图或简称为dag。dag有一个操作和一个参数列表。参数也可以是dag，允许构造复杂的图。例如，输出操作数为(outs GPROpnd:\$rd)。

outs操作将此dag表示为输出操作数列表。唯一的参数GPROpnd:\$rd由一个类型和一个名称组成，连接了之前看到的几个部分。类型是GPROnd，在前一节中定义的寄存器操作数的名称。名称\$rd指的是目标寄存器。前面的操作数字符串中使用了这个名称，并且在F\_L超类中也使用了这个名称，输入操作数的定义类似，类的其余部分初始化Inst字段的其他部分。请花点时间检查所有32位是否已经分配。

将最后的指令定义放在M88kInstrInfo.td文件中。由于每个逻辑指令都有两个变体，因此使用一个多类来同时定义这两个指令。这里也将指令选择的模式定义为有向无环图，设置模式中的操作，第一个参数是目标寄存器。第二个参数是一个嵌套图，这是实际的模式。同样，操作的名称是第一个OpNode元素。

LLVM有许多预定义的操作，这些操作可以在llvm/include/llvm/Target/TargetSelectionDAG.td文件中找到件(\url{https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/Target/TargetSelectionDAG.td})。例如，有and操作，表示位与操作。参数是两个源寄存器，\$rs1和\$rs2。大致可以这样阅读该模式:若指令选择的输入包含使用两个寄存器的OpNode操作，则该操作的结果赋值给\$rd寄存器，并生成该指令。利用图结构，可以定义更复杂的模式。例如，第二个模式使用非操作数将补码集成到模式中。

需要指出的一个小细节是，逻辑运算是可交换的。这对指令选择很有帮助，所以可以将这些指令的isCommutable标志设置为1:

\begin{cpp}
multiclass Logic<bits<5> Fun, string OpcStr, SDNode OpNode> {
    let isCommutable = 1 in
    def rr : F_LR<Fun, /*comp=*/0b0, OpcStr,
                    [(set i32:$rd,
                    (OpNode GPROpnd:$rs1, GPROpnd:$rs2))]>;
    def rrc : F_LR<Fun, /*comp=*/0b1, OpcStr,
                    [(set i32:$rd,
                    (OpNode GPROpnd:$rs1, (not GPROpnd:$rs2)))]>;
}
\end{cpp}

最后，定义指令的记录:

\begin{cpp}
defm AND : Logic<0b01000, "and", and>;
defm XOR : Logic<0b01010, "xor", xor>;
defm OR : Logic<0b01011, "or", or>;
\end{cpp}

第一个参数是函数的位模式，第二个是助记符，第三个参数是模式中使用的dag操作。

要完全理解类层次结构，重新查看类定义。指导设计原则是避免信息的重复。例如，0b01000函数位模式只使用一次。若没有Logic多类，需要输入此位模式两次，并重复该模式几次，这很容易出错。

注意，最好为说明建立一个命名方案。例如，and指令的记录命名为ANDrr，而带有补充寄存器的变体命名为ANDrrc。这些名称最终会出现在生成的C++源码，使用命名方案有助于理解该名称指的是哪条汇编指令。

目前，我们对m88k架构的寄存器文件进行了建模，并定义了一些指令。下一节中，我们将创建顶层文件。

\mySubsubsection{11.4.3.}{为目标描述创建顶层文件}

我们创建了M88kRegisterInfo.td，M88kInstrFormats.td和M88kInstrInfo.td的文件。目标描述是一个名为M88k.td的文件，该文件首先包含LLVM定义:

\begin{cpp}
include "llvm/Target/Target.td"

include "M88kRegisterInfo.td"
include "M88kInstrFormats.td"
include "M88kInstrInfo.td"
\end{cpp}

我们将在稍后添加更多后端功能时扩展这个include部分。

顶层文件还定义了一些全局实例，第一个名为M88kInstrInfo的记录保存了所有指令的信息:

\begin{cpp}
def M88kInstrInfo : InstrInfo;
\end{cpp}

我们将汇编类称为M88kAsmParser。为了使TableGen能够识别硬编码寄存器，指定寄存器名，以百分号为前缀，需要定义一个汇编解析器来指定:

\begin{cpp}
def M88kAsmParser : AsmParser;
def M88kAsmParserVariant : AsmParserVariant {
    let RegisterPrefix = "%";
}
\end{cpp}

最后，需要定义目标:

\begin{cpp}
def M88k : Target {
    let InstructionSet = M88kInstrInfo;
    let AssemblyParsers = [M88kAsmParser];
    let AssemblyParserVariants = [M88kAsmParserVariant];
}
\end{cpp}

现在，已经定义了足够的目标，可以编写第一个实用程序了。下一节中，我们将向LLVM添加M88k后端。





